Use IdentityArrayMap instead of HashMap inside DerivedState Updates DerivedState implementation to use IdentityArrayMap for faster iteration. This change exposes internal `keys` array of dependency map with potentially empty elements, which we have to skip over when reading. Changes `DerivedState.currentValue` to only return the value without forcing record of transitive dependency reads. Change-Id: Ica516bba042781f03e21fda7628b9788f9e018bd
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt index 1b652e8..65a96b1 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -710,7 +710,9 @@ // Record derived state dependency mapping if (value is DerivedState<*>) { derivedStates.removeScope(value) - value.dependencies.forEach { dependency -> + for (dependency in value.dependencies) { + // skip over empty objects from dependency array + if (dependency == null) break derivedStates.add(dependency, value) } } diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt index ee172c5..4563c63 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
@@ -18,6 +18,7 @@ @file:JvmMultifileClass package androidx.compose.runtime +import androidx.compose.runtime.collection.IdentityArrayMap import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf import androidx.compose.runtime.snapshots.Snapshot @@ -50,7 +51,7 @@ * The [dependencies] list can be used to determine when a [StateObject] appears in the apply * observer set, if the state could affect value of this derived state. */ - val dependencies: Set<StateObject> + val dependencies: Array<Any?> /** * Mutation policy that controls how changes are handled after state dependencies update. @@ -77,7 +78,7 @@ val Unset = Any() } - var dependencies: HashMap<StateObject, Int>? = null + var dependencies: IdentityArrayMap<StateObject, Int>? = null var result: Any? = Unset var resultHash: Int = 0 @@ -99,9 +100,9 @@ val dependencies = sync { dependencies } if (dependencies != null) { notifyObservers(derivedState) { - for ((stateObject, readLevel) in dependencies.entries) { + dependencies.forEach { stateObject, readLevel -> if (readLevel != 1) { - continue + return@forEach } if (stateObject is DerivedSnapshotState<*>) { @@ -140,9 +141,9 @@ // for correct invalidation later if (forceDependencyReads) { notifyObservers(this) { - val dependencies = readable.dependencies ?: emptyMap() + val dependencies = readable.dependencies val invalidationNestedLevel = calculationBlockNestedLevel.get() ?: 0 - for ((dependency, nestedLevel) in dependencies) { + dependencies?.forEach { dependency, nestedLevel -> calculationBlockNestedLevel.set(nestedLevel + invalidationNestedLevel) snapshot.readObserver?.invoke(dependency) } @@ -153,7 +154,7 @@ } val nestedCalculationLevel = calculationBlockNestedLevel.get() ?: 0 - val newDependencies = HashMap<StateObject, Int>() + val newDependencies = IdentityArrayMap<StateObject, Int>() val result = notifyObservers(this) { calculationBlockNestedLevel.set(nestedCalculationLevel + 1) @@ -218,18 +219,23 @@ // value is used instead which doesn't notify. This allow the read observer to read the // value and only update the cache once. Snapshot.current.readObserver?.invoke(this) - return currentValue + return first.withCurrent { + @Suppress("UNCHECKED_CAST") + currentRecord(it, Snapshot.current, true, calculation).result as T + } } override val currentValue: T get() = first.withCurrent { @Suppress("UNCHECKED_CAST") - currentRecord(it, Snapshot.current, true, calculation).result as T + currentRecord(it, Snapshot.current, false, calculation).result as T } - override val dependencies: Set<StateObject> + override val dependencies: Array<Any?> get() = first.withCurrent { - currentRecord(it, Snapshot.current, false, calculation).dependencies?.keys ?: emptySet() + val record = currentRecord(it, Snapshot.current, false, calculation) + @Suppress("UNCHECKED_CAST") + record.dependencies?.keys ?: emptyArray() } override fun toString(): String = first.withCurrent { diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt index 55240ef..3a7cf06 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -279,6 +279,8 @@ dependencyToDerivedStates.removeScope(value) val dependencies = value.dependencies for (dependency in dependencies) { + // skip over dependency array + if (dependency == null) break dependencyToDerivedStates.add(dependency, value) } derivedStateToValue[value] = value.currentValue